package edu.northwestern.cbits.purple_robot_manager.probes.builtin;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.jsoup.HttpStatusException;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningTaskInfo;
import android.app.usage.UsageEvents;
import android.app.usage.UsageStatsManager;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.preference.CheckBoxPreference;
import android.preference.PreferenceManager;
import android.preference.PreferenceScreen;
import android.provider.Settings;
import android.widget.Toast;
import edu.northwestern.cbits.purple_robot_manager.R;
import edu.northwestern.cbits.purple_robot_manager.WiFiHelper;
import edu.northwestern.cbits.purple_robot_manager.activities.settings.FlexibleListPreference;
import edu.northwestern.cbits.purple_robot_manager.logging.LogManager;
import edu.northwestern.cbits.purple_robot_manager.logging.SanityCheck;
import edu.northwestern.cbits.purple_robot_manager.logging.SanityManager;
import edu.northwestern.cbits.purple_robot_manager.probes.Probe;
public class RunningSoftwareProbe extends Probe
{
private static final String PACKAGE_NAME = "PACKAGE_NAME";
private static final String RUNNING_TASKS = "RUNNING_TASKS";
private static final String RUNNING_TASK_COUNT = "RUNNING_TASK_COUNT";
private static final String PACKAGE_CATEGORY = "PACKAGE_CATEGORY";
private static final String TASK_STACK_INDEX = "TASK_STACK_INDEX";
private static final boolean DEFAULT_ENABLED = true;
private static final String ENABLED = "config_probe_running_software_enabled";
private static final String FREQUENCY = "config_probe_running_software_frequency";
private static final String MUTE_ANDROID_FIVE_WARNING = "config_probe_running_software_mute_android_five_warning";
private static final boolean DEFAULT_ANDROID_FIVE_WARNING = false;
private long _lastCheck = 0;
@Override
public String getPreferenceKey() {
return "built_in_running_software";
}
@Override
public String name(Context context)
{
return "edu.northwestern.cbits.purple_robot_manager.probes.builtin.RunningSoftwareProbe";
}
@Override
public String title(Context context)
{
return context.getString(R.string.title_running_software_probe);
}
@Override
public String probeCategory(Context context)
{
return context.getResources().getString(R.string.probe_device_info_category);
}
@Override
public void enable(Context context)
{
SharedPreferences prefs = Probe.getPreferences(context);
Editor e = prefs.edit();
e.putBoolean(RunningSoftwareProbe.ENABLED, true);
e.commit();
}
@Override
public void disable(Context context)
{
SharedPreferences prefs = Probe.getPreferences(context);
Editor e = prefs.edit();
e.putBoolean(RunningSoftwareProbe.ENABLED, false);
e.commit();
}
@Override
public boolean isEnabled(final Context context)
{
final SharedPreferences prefs = Probe.getPreferences(context);
if (super.isEnabled(context))
{
final long now = System.currentTimeMillis();
if (prefs.getBoolean(RunningSoftwareProbe.ENABLED, RunningSoftwareProbe.DEFAULT_ENABLED))
{
synchronized (this)
{
long freq = Long.parseLong(prefs.getString(RunningSoftwareProbe.FREQUENCY, Probe.DEFAULT_FREQUENCY));
if (now - this._lastCheck > freq)
{
final RunningSoftwareProbe me = this;
Runnable r = new Runnable()
{
@Override
@SuppressWarnings("deprecation")
public void run()
{
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
{
ActivityManager am = (ActivityManager) context.getApplicationContext()
.getSystemService(Context.ACTIVITY_SERVICE);
List<RunningTaskInfo> tasks = am.getRunningTasks(9999);
Bundle bundle = new Bundle();
bundle.putString("PROBE", me.name(context));
bundle.putLong("TIMESTAMP", System.currentTimeMillis() / 1000);
ArrayList<Bundle> running = new ArrayList<>();
if (tasks != null)
{
for (int i = 0; i < tasks.size(); i++) {
RunningTaskInfo info = tasks.get(i);
Bundle taskBundle = new Bundle();
taskBundle.putString(RunningSoftwareProbe.PACKAGE_NAME, info.baseActivity.getPackageName());
taskBundle.putInt(RunningSoftwareProbe.TASK_STACK_INDEX, i);
String category = RunningSoftwareProbe.fetchCategory(context, info.baseActivity.getPackageName());
taskBundle.putString(RunningSoftwareProbe.PACKAGE_CATEGORY, category);
running.add(taskBundle);
}
bundle.putInt(RunningSoftwareProbe.RUNNING_TASK_COUNT, running.size());
bundle.putParcelableArrayList(RunningSoftwareProbe.RUNNING_TASKS, running);
me.transmitData(context, bundle);
}
}
else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
{
final SanityManager sanity = SanityManager.getInstance(context);
final String title = context.getString(R.string.title_app_usage_data_unavailable_rsp);
if (prefs.getBoolean(RunningSoftwareProbe.MUTE_ANDROID_FIVE_WARNING, RunningSoftwareProbe.DEFAULT_ANDROID_FIVE_WARNING) == false) {
final String message = context.getString(R.string.message_app_usage_data_unavailable_rsp);
sanity.addAlert(SanityCheck.WARNING, title, message, null);
}
else
sanity.clearAlert(title);
}
else
{
UsageStatsManager usage = (UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE);
synchronized(usage)
{
final SanityManager sanity = SanityManager.getInstance(context);
final String title = context.getString(R.string.title_app_usage_data_required);
final String message = context.getString(R.string.message_app_usage_data_required);
final long now = System.currentTimeMillis();
if (usage.queryEvents(now - (1 * 60 * 60 * 1000), now).hasNextEvent() == false)
{
Runnable action = new Runnable() {
@Override
public void run() {
Intent intent = new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
try
{
context.startActivity(intent);
sanity.clearAlert(title);
}
catch(Exception e)
{
LogManager.getInstance(context).logException(e);
LogManager.getInstance(context).logException(e);
Runnable r = new Runnable()
{
@Override
public void run() {
Toast.makeText(context, R.string.toast_missing_access_settings, Toast.LENGTH_LONG).show();
}
};
new Handler(Looper.getMainLooper()).post(r);
}
}
};
sanity.addAlert(SanityCheck.WARNING, title, message, action);
}
else
{
sanity.clearAlert(title);
me._lastCheck = now;
UsageEvents events = usage.queryEvents(now - (5 * 60 * 1000), now);
ArrayList<String> packages = new ArrayList<>();
UsageEvents.Event event = new UsageEvents.Event();
while (events.getNextEvent(event)) {
if (event.getEventType() == UsageEvents.Event.MOVE_TO_FOREGROUND)
{
String pkgName = new String(event.getPackageName());
if (packages.contains(pkgName))
packages.remove(pkgName);
packages.add(pkgName);
}
event = new UsageEvents.Event();
}
Bundle bundle = new Bundle();
bundle.putString("PROBE", me.name(context));
bundle.putLong("TIMESTAMP", System.currentTimeMillis() / 1000);
ArrayList<Bundle> running = new ArrayList<>();
Collections.reverse(packages);
for (int i = 0; i < packages.size(); i++) {
Bundle taskBundle = new Bundle();
String pkgName = packages.get(i);
taskBundle.putString(RunningSoftwareProbe.PACKAGE_NAME, pkgName);
taskBundle.putInt(RunningSoftwareProbe.TASK_STACK_INDEX, i);
String category = RunningSoftwareProbe.fetchCategory(context, pkgName);
taskBundle.putString(RunningSoftwareProbe.PACKAGE_CATEGORY, category);
running.add(taskBundle);
}
if (running.size() > 0) {
bundle.putInt(RunningSoftwareProbe.RUNNING_TASK_COUNT, running.size());
bundle.putParcelableArrayList(RunningSoftwareProbe.RUNNING_TASKS, running);
me.transmitData(context, bundle);
}
}
}
}
}
};
Thread t = new Thread(r);
t.start();
me._lastCheck = now;
}
}
return true;
}
}
return false;
}
protected static String fetchCategory(Context context, String packageName)
{
SharedPreferences prefs = Probe.getPreferences(context);
String key = "category_" + packageName;
if (prefs.contains(key))
return prefs.getString(key, context.getString(R.string.app_category_unknown));
try
{
// TODO: Replace with constant...
if (prefs.getBoolean("config_restrict_data_wifi", true))
{
if (WiFiHelper.wifiAvailable(context) == false)
return context.getString(R.string.app_category_unknown);
}
}
catch (ClassCastException e)
{
if (prefs.getString("config_restrict_data_wifi", "true").equalsIgnoreCase("true"))
{
Editor ed = prefs.edit();
ed.putBoolean("config_restrict_data_wifi", true);
ed.commit();
if (WiFiHelper.wifiAvailable(context) == false)
return context.getString(R.string.app_category_unknown);
}
else
{
Editor ed = prefs.edit();
ed.putBoolean("config_restrict_data_wifi", false);
ed.commit();
}
}
String category = null;
try
{
String url = "https://play.google.com/store/apps/details?id=" + packageName;
Document doc = Jsoup.connect(url).get();
Element detailsTab = doc.select("a.category").first();
if (detailsTab != null)
{
Elements spans = detailsTab.select("span");
for (Element span : spans)
{
category = span.text();
}
}
}
catch (HttpStatusException ex)
{
if (ex.getStatusCode() == 404)
category = context.getString(R.string.app_category_bundled);
else
LogManager.getInstance(context).logException(ex);
}
catch (IOException | NullPointerException ex)
{
LogManager.getInstance(context).logException(ex);
}
if (category == null)
category = context.getString(R.string.app_category_unknown);
Editor e = prefs.edit();
e.putString(key, category);
e.commit();
return category;
}
@Override
public String summarizeValue(Context context, Bundle bundle)
{
int count = (int) bundle.getDouble(RunningSoftwareProbe.RUNNING_TASK_COUNT);
return String.format(context.getResources().getString(R.string.summary_running_software_probe), count);
}
private Bundle bundleForTaskArray(Context context, ArrayList<Bundle> objects)
{
Bundle bundle = new Bundle();
ArrayList<String> keys = new ArrayList<>();
for (int i = 0; i < objects.size(); i++)
{
Bundle value = objects.get(i);
String name = value.getString(RunningSoftwareProbe.PACKAGE_NAME);
String key = String.format(context.getString(R.string.display_running_task_title), (i + 1));
keys.add(key);
bundle.putString(key, name);
}
bundle.putStringArrayList("KEY_ORDER", keys);
return bundle;
}
@Override
@SuppressWarnings("unchecked")
public Bundle formattedBundle(Context context, Bundle bundle)
{
Bundle formatted = super.formattedBundle(context, bundle);
ArrayList<Bundle> array = (ArrayList<Bundle>) bundle.get(RunningSoftwareProbe.RUNNING_TASKS);
int count = (int) bundle.getDouble(RunningSoftwareProbe.RUNNING_TASK_COUNT);
Bundle tasksBundle = this.bundleForTaskArray(context, array);
formatted.putBundle(String.format(context.getString(R.string.display_running_tasks_title), count), tasksBundle);
return formatted;
}
@Override
public Map<String, Object> configuration(Context context)
{
Map<String, Object> map = super.configuration(context);
SharedPreferences prefs = Probe.getPreferences(context);
long freq = Long.parseLong(prefs.getString(RunningSoftwareProbe.FREQUENCY, Probe.DEFAULT_FREQUENCY));
map.put(Probe.PROBE_FREQUENCY, freq);
boolean muteWarning = prefs.getBoolean(RunningSoftwareProbe.MUTE_ANDROID_FIVE_WARNING, RunningSoftwareProbe.DEFAULT_ANDROID_FIVE_WARNING);
map.put(Probe.PROBE_MUTE_WARNING, muteWarning);
return map;
}
@Override
public void updateFromMap(Context context, Map<String, Object> params)
{
super.updateFromMap(context, params);
SharedPreferences prefs = Probe.getPreferences(context);
Editor e = prefs.edit();
if (params.containsKey(Probe.PROBE_FREQUENCY)) {
Object frequency = params.get(Probe.PROBE_FREQUENCY);
if ((frequency instanceof Double) == false)
frequency = Double.valueOf(frequency.toString()).longValue();
else
frequency = ((Double) frequency).longValue();
e.putString(RunningSoftwareProbe.FREQUENCY, frequency.toString());
}
if (params.containsKey(Probe.PROBE_MUTE_WARNING))
{
Boolean muteWarning = (Boolean) params.get(Probe.PROBE_MUTE_WARNING);
e.putBoolean(RunningSoftwareProbe.MUTE_ANDROID_FIVE_WARNING, muteWarning);
}
e.commit();
}
@Override
public String summary(Context context)
{
return context.getString(R.string.summary_running_software_probe_desc);
}
@Override
@SuppressWarnings("deprecation")
public PreferenceScreen preferenceScreen(final Context context, PreferenceManager manager)
{
PreferenceScreen screen = super.preferenceScreen(context, manager);
screen.setTitle(this.title(context));
screen.setSummary(R.string.summary_running_software_probe_desc);
CheckBoxPreference enabled = new CheckBoxPreference(context);
enabled.setTitle(R.string.title_enable_probe);
enabled.setKey(RunningSoftwareProbe.ENABLED);
enabled.setDefaultValue(RunningSoftwareProbe.DEFAULT_ENABLED);
screen.addPreference(enabled);
FlexibleListPreference duration = new FlexibleListPreference(context);
duration.setKey(RunningSoftwareProbe.FREQUENCY);
duration.setEntryValues(R.array.probe_low_frequency_values);
duration.setEntries(R.array.probe_low_frequency_labels);
duration.setTitle(R.string.probe_frequency_label);
duration.setDefaultValue(Probe.DEFAULT_FREQUENCY);
screen.addPreference(duration);
CheckBoxPreference muteWarning = new CheckBoxPreference(context);
muteWarning.setTitle(R.string.title_mute_android_five_warning);
muteWarning.setKey(RunningSoftwareProbe.MUTE_ANDROID_FIVE_WARNING);
muteWarning.setDefaultValue(RunningSoftwareProbe.DEFAULT_ANDROID_FIVE_WARNING);
screen.addPreference(muteWarning);
return screen;
}
@Override
public JSONObject fetchSettings(Context context)
{
JSONObject settings = super.fetchSettings(context);
try
{
JSONArray values = new JSONArray();
values.put(true);
values.put(false);
JSONObject muteWarning = new JSONObject();
muteWarning.put(Probe.PROBE_TYPE, Probe.PROBE_TYPE_BOOLEAN);
muteWarning.put(Probe.PROBE_VALUES, values);
settings.put(Probe.PROBE_MUTE_WARNING, muteWarning);
JSONObject frequency = new JSONObject();
frequency.put(Probe.PROBE_TYPE, Probe.PROBE_TYPE_LONG);
values = new JSONArray();
String[] options = context.getResources().getStringArray(R.array.probe_low_frequency_values);
for (String option : options)
{
values.put(Long.parseLong(option));
}
frequency.put(Probe.PROBE_VALUES, values);
settings.put(Probe.PROBE_FREQUENCY, frequency);
}
catch (JSONException e)
{
LogManager.getInstance(context).logException(e);
}
return settings;
}
public String assetPath(Context context)
{
return "running-software-probe.html";
}
}